//
//  AppController.m
//  operator
//
//  Created by Andrej on 21.12.11.
//  Copyright (c) 2011 RWTH Aachen. All rights reserved.
//

#import "AppController.h"

@interface AppController(PrivateMethods)
    -(bool) isNearby:(NSPoint)point of:(CLLocationCoordinate2D)location;
    -(void) updateAnnotations;
    -(void) addAnnotationAt:(CLLocationCoordinate2D)location
                     type:(MapIconType)type
                   naming:(NSString*)name
              referencing:(NSString*)objectId
                  boundTo:(NSString*)bindingId;
    -(bool) isCorrectBindingFrom:(MapIcon*)annotation1 To:(MapIcon*)annotation2;
@end

@implementation AppController

@synthesize teamMemberPanel;
@synthesize checkpointPanel;

-(id) initWithWindow:(NSWindow *)window {
    self = [super initWithWindow:window];
    if (self) {
        /* initialize singleton object */
        state = [GameState defaultState]; 
        
        teamMemberPanel = [[NSMutableArray alloc] init];
        checkpointPanel = [[NSMutableArray alloc] init];
    }
    return self;
}

-(void) dealloc {
    [teamMemberPanel release];
    [checkpointPanel release];
    [super dealloc];
}

-(void) windowDidLoad {
    [super windowDidLoad];
}

-(void) awakeFromNib {
    /* register observer for receiving network messages */
    [[NSNotificationCenter defaultCenter] addObserver:self
                                           selector:@selector(receiveNetworkingNotification:) 
                                               name:NETWORKING_NOTIFICATION
                                             object:nil];

    /* set self as delegate for the map */
    [mapView setDelegate:self];

    /* set self as delegate for information tables */
    [teamTable setDelegate:self];
    [checkpointTable setDelegate:self];
}

#pragma Methods

/* 
 * Returns the key for the MapIcon-object in state.mapAnnotations which
 * is located at the position under the mouse click.
 * If there is no such object, the method will return nil.
 * (Note: Bases and enemy hunters are excluded)
 */
-(NSString*) getAnnotationLocatedAt:(NSPoint)p {
    /* For all annotations check if the annotation is "nearby" (calling
     isNearby:of:) and if it is not an enemy hunter or the base, return
     the key of this object. */
    for( id annotationId in state.mapAnnotations ) {
        MapIcon *annotation = (MapIcon*)[state.mapAnnotations objectForKey:annotationId];
        if( [self isNearby:p of:annotation.coordinate] && 
        annotation.type != MapIconTypeOtherHunter &&
        annotation.type != MapIconTypeBase ) {
            return [[NSString alloc] initWithString:annotation.ID];
        }
    }
    return nil;
}

/*
 * Checks if annotaionId is a key for an MapIcon-object in
 * state.mapAnnotations
 */
-(bool) isAnnotationIdValid:(NSString*)annotationId {
    return [state.mapAnnotations objectForKey:annotationId] != nil;
}

/*
 * For an annotationId which is a kay for an MapIcon-object in state.mapAnnotations
 * the transformation from latitude, longitude coordinates to screen coordinates
 * is done. Returns (0, 0) if the key is not valid.
 */
-(NSPoint) getAnnotationLocation:(NSString*)annotationId {
    MapIcon *annotation = (MapIcon*)[state.mapAnnotations objectForKey:annotationId];
    if( annotation != nil ) {
        CGPoint annoPoint = [mapView convertCoordinate:annotation.coordinate toPointToView:mapView];
        /* Here some adaption is needed, since the annotations coordinatates are not
           in the center of the icon, but rather at the bottom of the icon. */
        return NSMakePoint(annoPoint.x+1, annoPoint.y-8);
    }
    return NSMakePoint(-1, -1);
}

/*
 * Sends an assign message to the server if the binding of the corresponding game
 * objects (runners, hunters, checkpoints) is valid and returns true.
 * Otherwise the method returns false.
 */
-(bool) setBindingFrom:(NSString*)annotationId1 To:(NSString*)annotationId2 {
    /* get the mapicons for the given keys */
    MapIcon *anno1 = (MapIcon*)[state.mapAnnotations objectForKey:annotationId1];
    MapIcon *anno2 = (MapIcon*)[state.mapAnnotations objectForKey:annotationId2];

    /* check "right" direction */
    if( [self isCorrectBindingFrom:anno1 To:anno2] ) {
        NetworkMessage *ntwrMsg;
        
        /* check what type of message has to be send */
        if( anno1.type == MapIconTypeOwnRunner ) {
            ntwrMsg = [NetworkMessageCreator assignMessageWithRunner:[annotationId1 substringFromIndex:2] Checkpoint:[annotationId2 substringFromIndex:2] Assign:true];

        } else { //type == own hunter
            ntwrMsg = [NetworkMessageCreator assignMessageWithHunter:[annotationId1 substringFromIndex:2] Runner:[annotationId2 substringFromIndex:2] Assign:true];
        }
        
        /* send the message to the server */
        [[NetworkingCenter defaultServer] sendMessage:ntwrMsg];

        /* Update selected annotation's binding id, and replace it in the dictionary.
           This is done to show the binding immediately assuming everything will be
           alright ;) */
        [anno1 setBindingId:annotationId2];
        [state.mapAnnotations removeObjectForKey:annotationId1];
        [state.mapAnnotations setObject:anno1 forKey:annotationId1];

        return true;
    /* check "other" direction */
    } else if ( [self isCorrectBindingFrom:anno2 To:anno1] ) {
        NetworkMessage *ntwrMsg;
        /* check what type of message has to be send */
        if( anno2.type == MapIconTypeOwnRunner ) {
            ntwrMsg = [NetworkMessageCreator assignMessageWithRunner:[annotationId2 substringFromIndex:2] Checkpoint:[annotationId1 substringFromIndex:2] Assign:true];
        } else { //type == own hunter
            ntwrMsg = [NetworkMessageCreator assignMessageWithHunter:[annotationId2 substringFromIndex:2] Runner:[annotationId1 substringFromIndex:2] Assign:true];
        }
        /* send the message to the server */
        [[NetworkingCenter defaultServer] sendMessage:ntwrMsg];

        /* Update selected annotation's binding id, and replace it in the dictionary.
         This is done to show the binding immediately assuming everything will be
         alright ;) */
        [anno2 setBindingId:annotationId1];
        [state.mapAnnotations removeObjectForKey:annotationId2];
        [state.mapAnnotations setObject:anno2 forKey:annotationId2];

        return true;
    }
    return false;
}

/*
 * Sends an unassign message to the server if there exist a binding for the 
 * corresponding game object (runner, hunter) and returns true.
 * Otherwise the method retruns false.
 */
-(bool) unsetBindingOf:(NSString*)annotationId {
    /* get the mapicon fot the given key */
    MapIcon *anno = (MapIcon*)[state.mapAnnotations objectForKey:annotationId];

    /* can only unbind existing binding from team runners/hunters */
    if( anno != nil && (anno.type == MapIconTypeOwnHunter || anno.type == MapIconTypeOwnRunner)
    && anno.bindingId != nil ) {
        NetworkMessage *ntwrMsg;
        /* check what type of message has to be send */
        if( anno.type == MapIconTypeOwnRunner ) {
            ntwrMsg = [NetworkMessageCreator assignMessageWithRunner:[[anno ID] substringFromIndex:2] Checkpoint:[[anno bindingId] substringFromIndex:2] Assign:false];
        } else { //type == own hunter
            ntwrMsg = [NetworkMessageCreator assignMessageWithHunter:[[anno ID] substringFromIndex:2] Runner:[[anno bindingId] substringFromIndex:2] Assign:false];
        }
        /* send the message to the server */
        [[NetworkingCenter defaultServer] sendMessage:ntwrMsg];

        /* Update selected annotation's binding id, and replace it in the dictionary.
         This is done to show the binding immediately assuming everything will be
         alright ;) */
        [anno setBindingId:nil];
        [state.mapAnnotations removeObjectForKey:anno.ID];
        [state.mapAnnotations setObject:anno forKey:anno.ID];

        return true;
    }
    return false;
}

/*
 * Returns true if the binding from the game object corresponding to annotationId1
 * to the game object corresponding to annotationId2 (or vice-versa) is valid.
 * Otherwise the method retruns false.
 */
-(bool) isValidBindingBetween:(NSString*)annotationId1 And:(NSString*)annotationId2 {
    if( annotationId1 == nil || annotationId2 == nil ) {
    return false;
    }

    MapIcon *anno1 = (MapIcon*)[state.mapAnnotations objectForKey:annotationId1];
    MapIcon *anno2 = (MapIcon*)[state.mapAnnotations objectForKey:annotationId2];

    return [self isCorrectBindingFrom:anno1 To:anno2] || [self isCorrectBindingFrom:anno2 To:anno1];
}

#pragma Private Methods

/*
 * Returns true if the give coordinate location is in a 22x22 pixel area (with
 * given point as center) visible on the screen.
 * Otherwise the method retruns false.
 */
-(bool) isNearby:(NSPoint)point of:(CLLocationCoordinate2D)location {
    CGPoint annoPoint = [mapView convertCoordinate:location toPointToView:mapView];
    //NSLog(@"(%f, %f) mapsto (%f, %f)", location.latitude, location.longitude, annoPoint.x, annoPoint.y);
    NSRect rect = NSMakeRect(annoPoint.x-11, annoPoint.y-22, 22, 22);
    return NSPointInRect(point, rect);
}

/* 
 * Updates map annotations and table data.
 * Note: this is more like update game state
 */
-(void) updateAnnotations {
    NSLog(@"Update annotations...");
    /* empty array of team members and checkpoints */
    [teamMemberPanel removeAllObjects];
    [checkpointPanel removeAllObjects];

    /* set all annotations to deprecated (instead of just removing all annotations) */
    for( id annotationId in state.mapAnnotations ) {
        MapIcon *annotation = (MapIcon*)[state.mapAnnotations objectForKey:annotationId];
        annotation.updated = false;
    }

    /* add annotations for checkpoints */
    for(id checkpointId in state.checkpoints) {
        Checkpoint *c = (Checkpoint*)[state.checkpoints objectForKey:checkpointId];
        CLLocationCoordinate2D loc;
        loc.latitude = [c position_lat];
        loc.longitude = [c position_long];
        [self addAnnotationAt:loc type:MapIconTypeCheckpoint naming:[NSString stringWithFormat:@"%d Points", c.points] referencing:c.checkpointID boundTo:nil];
    }

    /* add annotations for players */
    for(id teamId in state.teams) {
        Team *t = (Team*)[state.teams objectForKey:teamId];
        if( [state.teamId isEqualToString:teamId] ) {
            /* set base annotation */
            CLLocationCoordinate2D baseLoc;
            baseLoc.latitude = t.base_lat;
            baseLoc.longitude = t.base_long;
            [self addAnnotationAt:baseLoc type:MapIconTypeBase naming:@"Base" referencing:[state teamId] boundTo:nil];

            /* set status */
            state.teamPoints = [NSString stringWithFormat:@"%d", [t current_points]];
            state.teamMoney = [NSString stringWithFormat:@"%d", [t current_money]];
        }

        /* set player annotations */
        for(id playerId in t.players) {
            Player *p = (Player*)[t.players objectForKey:playerId];
            if( [p conformsToProtocol:@protocol(Located)] ) {
                LocatedPlayer *lp = (LocatedPlayer*)p;
                CLLocationCoordinate2D loc;
                loc.latitude = [lp latitude];
                loc.longitude = [lp longitude];
                MapIconType type;
                NSString *bindingId = nil;
                if( [state.teamId isEqualToString:teamId] ) {
                    if( [[p className] isEqualToString:@"Runner"] ) {
                        type = MapIconTypeOwnRunner;
                        if( [(Runner*)p checkpointID] != nil ) {
                            bindingId = [NSString stringWithFormat:@"%d:%@", MapIconTypeCheckpoint, [(Runner*)p checkpointID]];
                        }
                    } else { // if ( [[p className] isEqualToString:@"Hunter"] )
                        type = MapIconTypeOwnHunter;
                        if( [(Hunter*)p runnerID] != nil ) {
                            bindingId = [NSString stringWithFormat:@"%d:%@", MapIconTypeOtherRunner, [(Hunter*)p runnerID]];
                        }
                    }
                } else {
                    if( [[p className] isEqualToString:@"Runner"] ) {
                        type = MapIconTypeOtherRunner; 
                    } else { // if ( [[p className] isEqualToString:@"Hunter"] )
                        type = MapIconTypeOtherHunter;
                    }
                }
                [self addAnnotationAt:loc type:type naming:lp.name referencing:lp.clientID boundTo:bindingId];
            }
        }
    }

    /* remove non-updated (deprecated) annotations from the map */
    NSMutableDictionary *deprecatedAnnotations = [[NSMutableDictionary alloc] init];
    for( id annotationId in state.mapAnnotations ) {
        MapIcon *annotation = (MapIcon*)[state.mapAnnotations objectForKey:annotationId];
        if( annotation.updated == false ) {
            [deprecatedAnnotations setObject:annotation forKey:annotationId];
        }
    }
    // remove from map
    [mapView removeAnnotations:[deprecatedAnnotations allValues]];
    // remove from set
    [state.mapAnnotations removeObjectsForKeys:[deprecatedAnnotations allKeys]];
    [deprecatedAnnotations release];
}

/*
 * This method adds (or updates) an annotation on the map view.
 */
-(void) addAnnotationAt:(CLLocationCoordinate2D)location
                   type:(MapIconType)type
                 naming:(NSString*)name
            referencing:(NSString*)objectId
                boundTo:(NSString*)bindingId {
    /* The key of the annotation is of the form <type>:<id> */
    NSString* key = [NSString stringWithFormat:@"%d:%@", type, objectId];

    /* Check if there exists already such an annotation*/
    MapIcon* annotation = [(MapIcon*)[state.mapAnnotations objectForKey:key] retain];
    if( annotation == nil ) { /* it is a new annotation */
        NSLog(@"New annotation with id %@", key);
        /* Create a new annotation object and... */
        annotation = [[MapIcon alloc] initWithCoordinate:location
                                          annotationType:type
                                            annotationId:key
                                                   title:[name copy]];
        annotation.bindingId = bindingId;

        /* ...add it to the game state... */
        [state.mapAnnotations setObject:[annotation retain] forKey:key];
        /* ...and map. */
        [mapView addAnnotation:[annotation retain]];
    } else { /* annotation key already exists */
        /* This flag is used to check if the annotation has moved or changed its title */
        bool resetMapAnnotation = location.latitude != annotation.coordinate.latitude ||
                                  location.longitude != annotation.coordinate.longitude ||
                                  [annotation.title isEqualToString:name] == false;

        /* Update annotation in state.mapAnnotations dictionary */
        [annotation setCoordinate:location];
        annotation.title      = [name copy];
        annotation.bindingId  = bindingId;
        annotation.updated    = true;
        [state.mapAnnotations setObject:[annotation retain] forKey:key];

        /* Remove/add annotation on the map if it has changed (see flag) */
        if( resetMapAnnotation ) {
            [mapView removeAnnotation:annotation];
            [mapView addAnnotation:annotation];
        }
    }

    /* update table data (actually, this does not belong here 
     w.r.t. the symantic of this method...) */
    if(type == MapIconTypeOwnHunter || type == MapIconTypeOwnRunner) {
        [teamMemberPanel addObject:annotation];
    }
    else if (type == MapIconTypeCheckpoint) {
        [checkpointPanel addObject: annotation];
    }
    /* ensure data in the information tables is updated */
    [checkpointController rearrangeObjects];
    [memberController rearrangeObjects];
}

/*
 * Returns true if the game logic allows the binding between to game objects.
 * This is a directed check, i.e., in general (a, b) != (b, a).
 * Otherwise this method returns false.
 */
-(bool) isCorrectBindingFrom:(MapIcon*)annotation1 To:(MapIcon*)annotation2 {
    if( annotation1 != nil && annotation2 != nil ) {
        return (annotation1.type == MapIconTypeOwnHunter && annotation2.type == MapIconTypeOtherRunner) || (annotation1.type == MapIconTypeOwnRunner && annotation2.type == MapIconTypeCheckpoint);
    }
    return false;
}

#pragma Actions

-(IBAction) login:(id)sender {
    /* Set the team id to send to the server. */
    int teamId = (int)[teamChoose selectedRow];
    NSLog(@"Team %d choosed...", teamId);
    state.teamId = [NSString stringWithFormat:@"%d", teamId];
    
    #if OFFLINE != 1
        /* Send login message to the server with name, role (operator = 2), and team id */
        NetworkMessage *ntwrMsg = [NetworkMessageCreator loginMessageWith:[userTextField stringValue] role:2 team:teamId];
        [[NetworkingCenter defaultServer] sendMessage:ntwrMsg];

        /* --- IGNORE START (only for OFFLINE modus) --- */
    #else
        /* Create mapicons manually, and set timer up to show icon movements */
        state.running = true;

        #if SHOWPANELS == 1
        if( ![statusPanel isVisible] ) {
            [statusPanel orderBack:nil];
        }
        if( ![statusTeamPanel isVisible] ) {
            [statusTeamPanel orderBack:nil];
        }
        #endif

        state.teams = [[NSMutableDictionary alloc] init];
        state.checkpoints = [[NSMutableDictionary alloc] init];

        Runner *r1 = [[Runner alloc] initWithName:@"r1" andClientID:@"r1" andTeamID:@"0"];
        r1.last_position_lat = 50.779225;
        r1.last_position_long = 6.080715;
        r1.checkpointID = @"0";
        Runner *r2 = [[Runner alloc] initWithName:@"r2" andClientID:@"r2" andTeamID:@"1"];
        r2.last_position_lat = 50.776273;
        r2.last_position_long = 6.08163;

        Hunter *h1 = [[Hunter alloc] initWithName:@"h1" andClientID:@"h1" andTeamID:@"0"];
        h1.last_position_lat = 50.776049;
        h1.last_position_long = 6.083687;
        Hunter *h2 = [[Hunter alloc] initWithName:@"h2" andClientID:@"h2" andTeamID:@"1"];
        h2.last_position_lat = 50.773204;
        h2.last_position_long = 6.082795;

        Team *t1 = [[Team alloc] initWithName:@"0"];
        t1.base_lat = 50.778969;
        t1.base_long = 6.059024;
        [t1.players setObject:r1 forKey:@"r1"];
        [t1.players setObject:h1 forKey:@"h1"];

        Team *t2 = [[Team alloc] initWithName:@"1"];
        [t2.players setObject:r2 forKey:@"r2"];
        [t2.players setObject:h2 forKey:@"h2"];

        [state.teams setObject:t1 forKey:@"0"];
        [state.teams setObject:t2 forKey:@"1"];

        Checkpoint *cp1 = [[Checkpoint alloc] initWithID:@"0" andLong:6.079022 andLat:50.778070 andPoints:10];
        Checkpoint *cp2 = [[Checkpoint alloc] initWithID:@"1" andLong:6.088022 andLat:50.777273 andPoints:50];

        [state.checkpoints setObject:cp1 forKey:@"0"];
        [state.checkpoints setObject:cp2 forKey:@"1"];

        [NSTimer scheduledTimerWithTimeInterval: 1
                                       target: self
                                     selector:@selector(tick:)
                                     userInfo: nil
                                      repeats: YES];
    #endif
}

/* 
 * Create a path for the icons that will be traversed to show movements
 */
#if OFFLINE == 1
    int counter = 0;
    const int n = 20;
    CGFloat r1Path[n] = {50.779325, 6.080815,
                         50.779400, 6.080885,
                         50.779510, 6.080955,
                         50.779430, 6.081035,
                         50.779360, 6.081115,
                         50.779250, 6.081235,
                         50.779150, 6.081155,
                         50.779090, 6.081000,
                         50.779150, 6.080850,
                         50.779225, 6.080715};

    /*
     * Create a timer to show icon movements
     */
    -(void) tick:(NSTimer *)theTimer {
        NSLog(@"OFFLINE-TICK");
        Team *t1 = (Team*)[state.teams objectForKey:@"0"];

        LocatedPlayer* player = (LocatedPlayer*)[t1.players objectForKey:@"r1"];
        player.last_position_lat  = r1Path[counter++];
        player.last_position_long = r1Path[counter++];
        counter = counter % n;

        [self updateAnnotations];
        [overlayView setNeedsDisplay:true];
    }
#endif
/* --- IGNORE END --- */

-(IBAction) togglePanels:(id)sender {
    if( [statusPanel isVisible] ) {
        [statusPanel orderOut:nil];
        [statusTeamPanel orderOut:nil];
    } else {
        [statusPanel orderBack:nil];
        [statusTeamPanel orderBack:nil];
    }
}

-(IBAction) gotoBase:(id)sender {
    NSString *annoId = [NSString stringWithFormat:@"%d:%@", MapIconTypeBase, [state teamId]];
    MapIcon *annotation = (MapIcon*)[state.mapAnnotations objectForKey:annoId];
    if( annotation != nil ) {
        NSLog(@"Center map to %@", annoId);
        [mapView setCenterCoordinate:[annotation coordinate] animated:true];
        [overlayView setNeedsDisplay:true];
    }
}

#pragma Network stuff

/*
 * This is the callback method for any incomming network messages.
 */
-(void) receiveNetworkingNotification:(NSNotification *) notification {
    /* Run this method only for network notifications */
    if (![[notification name] isEqualToString:NETWORKING_NOTIFICATION]) return;

    NSLog(@"Received Notification...");

    NetworkMessage *ntwrMsg = (NetworkMessage*)[notification object];
    
    switch (ntwrMsg.type) {
        /* Here comes the login OK mesage. */
        case kLoginReplyMessage:
            NSLog(@"Login successful: %@", [ntwrMsg data]);
            /* Set state to running */
            state.running = true;
            #if SHOWPANELS == 1
                /* Set Panels to visible */
                if( ![statusPanel isVisible] ) {
                    [statusPanel orderBack:nil];
                }
                if( ![statusTeamPanel isVisible] ) {
                    [statusTeamPanel orderBack:nil];
                }
            #endif
            break;
          
        /* The message is an logout OK message. */
        case kLogoutReplyMessage:
            NSLog(@"Logout successful: %@", [ntwrMsg data]);
            /* Set running state to false */
            state.running = false;
            break;
          
        /* This message brings some nice game state updates ;)
           This is the keyframe we all are waiting for... */
        case kKeyframeOpMessage:
            NSLog(@"Keyframe: %@", [ntwrMsg data]);
            NSMutableDictionary *dataFromServer = (NSMutableDictionary*) [ntwrMsg data];
            /* Just delete all old stuff... */
            if( state.checkpoints != nil ) {
                [state.checkpoints release];
            }
            if( state.teams != nil ) {
                [state.teams release];
            }
            /* ...and use the data we got from the server. */
            state.checkpoints = [(NSMutableDictionary*)[dataFromServer objectForKey:@"checkpoints"] retain];
            state.teams = [(NSMutableDictionary*)[dataFromServer objectForKey:@"teams"] retain];

            /* update annotations and overlay view */
            [self updateAnnotations];
            [overlayView setNeedsDisplay:true];
            break;
          
        default:
            break;
    }
}

#pragma MapView Delegate

/*
 * This method is called when the map has finished loading.
 */
-(void )mapViewDidFinishLoadingMap:(MKMapView *)mv {
    NSLog(@"Initiliaze map...");
    /* Initilize map with coordinates and zoom level. */
    MKCoordinateSpan span;
    MKCoordinateRegion region;

    span.latitudeDelta = 0.005;
    span.longitudeDelta = 0.005;

    /* Set center of map to somewhere in Aachen */
    CLLocationCoordinate2D loc;
    loc.latitude = 50.775484;
    loc.longitude = 6.073;
    region.center = loc;
    region.span = span;

    [mv setRegion:region];

    /* Add some nice css styles for the map. */
    NSString *path = [[NSBundle mainBundle] pathForResource:@"MapView" ofType:@"css"];
    [mapView performSelector:@selector(addStylesheetTag:) withObject:path];

    /* Update annotations. */
    [self updateAnnotations];
}

/*
 * This method is called if we place a new annotation on a map.
 * It is used to specify which icon/image to use for the annotation.
 */
-(MKAnnotationView *) mapView:(MKMapView *)mv viewForAnnotation:(id <MKAnnotation>)annotation {
    // determine the type of annotation, and produce the correct type of annotation view for it.
    MapIcon* myAnnotation = (MapIcon *)annotation;
    NSString* identifier = nil;

    switch(myAnnotation.type) {
        case MapIconTypeCheckpoint:  identifier = @"checkpoint"; break;
        case MapIconTypeBase:        identifier = @"base"; break;
        case MapIconTypeOwnRunner:   identifier = @"ownRunner"; break;
        case MapIconTypeOwnHunter:   identifier = @"ownHunter"; break;
        case MapIconTypeOtherRunner: identifier = @"otherRunner"; break;
        case MapIconTypeOtherHunter: identifier = @"otherHunter"; break;
    }

    MKAnnotationView *annotationView = nil;
    if( identifier != nil ) {
        annotationView = (MapIconView *)[mv dequeueReusableAnnotationViewWithIdentifier:identifier];
        if( annotationView == nil ) {
            annotationView = [[[MapIconView alloc] initWithAnnotation:myAnnotation reuseIdentifier:identifier] autorelease];
        } else {
            [annotationView setAnnotation:myAnnotation];
        }

        [annotationView setEnabled:YES];
        if( myAnnotation.type == MapIconTypeCheckpoint || myAnnotation.type == MapIconTypeBase ) {
            [annotationView setCanShowCallout:YES];
        }
    }

    return annotationView;
}

#pragma TableView Delegate

/*
 * This method is called after a click on a cell in a panel table.
 */
-(BOOL) tableView:(NSTableView *)tableView shouldTrackCell:(NSCell *)cell forTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row {
    if( [[tableView tableColumns] count] > 0 ) {
        /* Assume that in the column with index 0 contains annotations keys.
           Thus, select the "first" cell of the current row. */
        NSTableColumn* column = (NSTableColumn*)[[tableView tableColumns] objectAtIndex:0];
        NSCell* cell = (NSCell*)[column dataCellForRow:row];
        NSString* annoId = [cell stringValue];

        /* Get the actual annotation. */
        MapIcon *annotation = (MapIcon*)[state.mapAnnotations objectForKey:annoId];
        if( annotation != nil ) {
            NSLog(@"Center map to %@", annoId);
            /* Center map to this annotation. */
            [mapView setCenterCoordinate:[annotation coordinate] animated:true];
            [overlayView setNeedsDisplay:true];
        }
    }

    return false;
}

@end